/**
 *
 * The MIT License
 *
 * Copyright (c) 2008 Board of Trustees, Leland Stanford Jr. University. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:

 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.

 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.macdadi.ks.client.views;

import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.*;
import org.macdadi.core.client.auth.AuthService;
import org.macdadi.core.client.auth.AuthenticatedUser;
import org.macdadi.core.client.auth.AuthorizationException;
import org.macdadi.core.client.domain.DataService;
import org.macdadi.core.client.domain.DataWriteService;
import org.macdadi.core.client.domain.Goal;
import org.macdadi.core.client.domain.ProjectInfo;
import org.macdadi.core.client.ui.DefaultItemEditor;
import org.macdadi.core.client.ui.EditableTree;
import org.macdadi.ks.client.View;
import org.macdadi.ks.client.ViewContainer;

import java.util.ArrayList;

/**
 * @author Ben Suter <ben.suter@gmail.com>
 */
public class Goals extends View implements TreeListener {
    protected EditableTree tree;
    protected ArrayList expandedItems = new ArrayList();
    protected ProjectInfo project;

    protected EditableTree.ItemEditor goalEdit, goalAdd, goalRemove;

    public static ViewInfo init() {
        String info = "<p>As a team, arrive at a common hierarchy of goals</p>";

        return new ViewInfo("Goals", info) {
            public View createInstance(ViewContainer container) {
                return new Goals(container);
            }

            public String getColor() {
                return "#fe9915";
            }
        };
    }

    public Goals(ViewContainer container) {
        this.container = container;
        project = container.getCurrentProject();

        HorizontalPanel hp = new HorizontalPanel();
        hp.setStyleName("macdadi-Goals");
        hp.setWidth("100%");

        tree = new EditableTree();
        hp.add(tree);
        tree.addTreeListener(this);

        goalEdit = new GoalEdit(updateTree, project);
        goalAdd = new GoalAdd(expandSelectedThenUpdateTree, project);
        goalRemove = new GoalRemove(updateTree, project);

        initWidget(hp);
    }

    public void onShow() {
        populateTree(project);
        AuthenticatedUser user = container.getCurrentUser();

        /* If not authenticated, we don't add any editors */
        if ( user == null )
        	return;

        if ( user.isAuthorizedOperation(project.getId(), AuthService.TARGET_GOALS, AuthService.OPERATION_CREATE))
            tree.addItemEditor(goalAdd);

        if ( user.isAuthorizedOperation(project.getId(), AuthService.TARGET_GOALS, AuthService.OPERATION_REMOVE))
            tree.addItemEditor(goalRemove);

        if ( user.isAuthorizedOperation(project.getId(), AuthService.TARGET_GOALS, AuthService.OPERATION_UPDATE))
            tree.addItemEditor(goalEdit);
    }

    public void onHide() {
        tree.stopEditing();
        tree.removeItemEditor(goalAdd);
        tree.removeItemEditor(goalRemove);
        tree.removeItemEditor(goalEdit);
        tree.clear();
    }

    /**
     * Add a new root item to the tree, whose label initially displays a "loading ..." message.
     * Then it retrieves all the root-level goals for this project and adds each one to the
     * tree. As it does so, the children of these goals are added recursively. See addChild().
     * <p/>
     * Finally the rootItem is expanded, so that the first level of goals
     * is always visible.
     *
     * @param project the project from which the tree domain should be pulled
     */
    protected void populateTree(final ProjectInfo project) {
        final TreeItem rootItem = new TreeItem();
        rootItem.setText("Loading goals for " + project.getName());
        tree.addItem(rootItem);

        DataService.App.getInstance().getGoals(project.getId(), container.getCurrentAuthToken(), new AsyncCallback() {
            public void onFailure(Throwable caught) {
                GWT.log("Failed to populate the goals tree", caught);
                if (caught instanceof AuthorizationException) {
                    rootItem.setText("Please login to view goals for " + project.getName());
                }
            }

            public void onSuccess(Object result) {
                rootItem.setText("Goals for " + project.getName());

                Goal[] roots = (Goal[]) result;
                for (int i = 0; i < roots.length; i++) {
                    addChild(rootItem, roots[i], true);
                }

                /* Expand the first and second levels */
                rootItem.setState(true, true);
                for (int i = 0; i < rootItem.getChildCount(); i++ ) {
                    rootItem.getChild(i).setState(true, true);
                }
            }
        });
    }

    /**
     * Adds the given Goal as a child TreeItem to the parent TreeItem.
     * Optionally steps into the given Goal's children and does the same there,
     * etc. recursively.
     *
     * @param parent  the tree node to act as parent for the newly added goal
     * @param child   the goal to be added as a child node to the tree
     * @param recurse if true, will call this method for the child and each of its children, etc.
     */
    protected void addChild(TreeItem parent, Goal child, boolean recurse) {
        TreeItem node = new TreeItem(child.getName());
        node.setUserObject(child);
        parent.addItem(node);

        if (recurse) {
            for (int i = 0; i < child.getNumChildren(); i++)
                addChild(node, child.getChild(i), true);
        }

        /* If this node, as identified by its goal's id, was "expanded" in a
         * previous incarnation of this tree, then expand it here again.
         */
        if (expandedItems.contains(Integer.toString(child.getId())))
            node.setState(true, false);
    }


    public void onTreeItemSelected(TreeItem item) {

    }

    /**
     * Remember the expansion state of this item, so that we can later
     * return the tree to the same state when it is refreshed.
     *
     * @param item the item whose expanded/folded state changed
     */
    public void onTreeItemStateChanged(TreeItem item) {
        /* Remember the expansion state for later tree refresh, based on goal ID. */
        Goal g;
        if (item.getUserObject() instanceof Goal)
            g = (Goal) item.getUserObject();
        else
            return;

        if (item.getState())
            expandedItems.add(Integer.toString(g.getId()));
        else
            expandedItems.remove(Integer.toString(g.getId()));
    }


    /**
     * A command that refreshes the tree.
     */
    protected Command updateTree = new Command() {
        public void execute() {
            tree.clear();
            populateTree(project);
        }
    };

    /**
     * A command that first expands the selected tree item, and then
     * refreshes the tree. This is typically used when a new child
     * gets added to a node, so that the new child will be visible
     * after the refresh.
     */
    protected Command expandSelectedThenUpdateTree = new Command() {
        public void execute() {
            /* If the item being edited is not already expanded, "remember"
             * it as expanded for after the refresh.
             */
            TreeItem edited = tree.getSelectedItem();
            if (edited != null) {
                if (edited.getUserObject() instanceof Goal) {
                    Goal g;
                    g = (Goal) edited.getUserObject();
                    String id;
                    id = Integer.toString(g.getId());
                    if (!expandedItems.contains(id))
                        expandedItems.add(id);
                }
            }

            updateTree.execute();
        }
    };

    

    /**
     * Collects functionality common to all editors related to Goal nodes.
     */
    public abstract class GoalItemEditor extends DefaultItemEditor {
        protected ProjectInfo project;

        public GoalItemEditor(String iconPath, String title, Command onSuccess, ProjectInfo p) {
            super(iconPath, title, onSuccess);
            this.project = p;
        }

        protected Widget getInputWidget(Object userObject) {
            if (userObject == null || !(userObject instanceof Goal))
                throw new IllegalArgumentException("userObject must be a valid Goal");

            return configureWidget((Goal) userObject);
        }

        protected abstract Widget configureWidget(final Goal goal);
    }

    /**
     * Lets you edit the Goal's name. Only matches to TreeItems
     * whose userObject is a Goal.
     */
    public class GoalEdit extends GoalItemEditor {
        public GoalEdit(Command onSuccess, ProjectInfo p) {
            super(DefaultItemEditor.iconPathEdit, "Edit", onSuccess, p);
        }

        public boolean matchesItem(TreeItem item) {
            /* Only display this for non-root items */
            return (item.getParentItem() != null);
        }

        protected Widget configureWidget(final Goal goal) {
            /* A panel for editing the name: text input, apply, cancel */
            VerticalPanel vpE = new VerticalPanel();
            final TextBox tbEdit = new TextBox();
            tbEdit.setVisibleLength(50);
            tbEdit.setText(goal.getName());
            vpE.add(tbEdit);

            HorizontalPanel hpE = new HorizontalPanel();

            Button btnEditDo = new Button("Apply");
            btnEditDo.addClickListener(new ClickListener() {
                public void onClick(Widget sender) {
                    String newName = tbEdit.getText().trim();
                    updateGoal(goal, newName);
                }
            });
            hpE.add(btnEditDo);

            Button btnEditCancel = new Button("Reset");
            btnEditCancel.addClickListener(new ClickListener() {
                public void onClick(Widget sender) {
                    tbEdit.setText(goal.getName());
                }
            });
            hpE.add(btnEditCancel);
            vpE.add(hpE);

            return vpE;
        }

        protected void updateGoal(final Goal goal, String newName) {
            if (newName.equals(goal.getName()))
                return; /* No change was made */

            DataWriteService.App.getInstance().updateGoal(project.getId(), goal.getId(), newName, container.getCurrentAuthToken(), this);
        }
    }

    /**
     * Lets you add a new goal as a child of the current goal,
     * providing an input field for the name of the new goal.
     * <p/>
     * If added to a TreeItem that does not contain a goal as
     * its userObject, then the new goal is added as a root-level
     * goal, not as a sub-goal.
     * <p/>
     * This editor matches any TreeItem.
     */
    public class GoalAdd extends GoalItemEditor {
        public GoalAdd(Command onSuccess, ProjectInfo p) {
            super(DefaultItemEditor.iconPathAdd, "Add a Sub-Goal", onSuccess, p);
        }

        public boolean matchesItem(TreeItem item) {
            return true;
        }

        protected Widget getInputWidget(Object userObject) {
            return configureWidget((Goal) userObject);
        }

        protected Widget configureWidget(final Goal goal) {
            /* A panel for editing the name: text input, apply, cancel */
            VerticalPanel vpA = new VerticalPanel();
            final TextBox tbAdd = new TextBox();
            tbAdd.setVisibleLength(50);
            vpA.add(tbAdd);
            HorizontalPanel hpA = new HorizontalPanel();
            Button btnAddDo = new Button("Create");
            btnAddDo.addClickListener(new ClickListener() {
                public void onClick(Widget sender) {
                    addChildGoal(goal, tbAdd.getText().trim());
                }
            });
            hpA.add(btnAddDo);

            Button btnAddCancel = new Button("Reset");
            btnAddCancel.addClickListener(new ClickListener() {
                public void onClick(Widget sender) {
                    tbAdd.setText("");
                }
            });
            hpA.add(btnAddCancel);
            vpA.add(hpA);

            return vpA;
        }

        protected void addChildGoal(final Goal goal, String newName) {
            if (newName.length() == 0) {
                displayError("You must enter a name for the new goal", null);
                return;
            }

            int parent = -1;
            if (goal != null)
                parent = goal.getId();

            DataWriteService.App.getInstance().createGoal(project.getId(), parent, newName, container.getCurrentAuthToken(), this);
        }
    }

    /**
     * Displays a clickable link to remove the goal and all its sub-goals.
     * <p/>
     * This editor only matches TreeItem's whose userObject is a Goal.
     */
    public class GoalRemove extends GoalItemEditor {
        public GoalRemove(Command onSuccess, ProjectInfo p) {
            super(DefaultItemEditor.iconPathRemove, "Remove", onSuccess, p);
        }

        protected Widget configureWidget(final Goal goal) {
            HTML rem = new HTML("<u style='cursor:hand'>Remove this goal (and all its sub goals)</u>");
            //Button rem = new Button("Remove this goal (and all its sub goals)");
            rem.addClickListener(new ClickListener() {
                public void onClick(Widget sender) {
                    removeGoalAndChildren(goal);
                }
            });

            return rem;
        }

        protected void removeGoalAndChildren(Goal goal) {
            DataWriteService.App.getInstance().removeGoal(project.getId(), goal.getId(), container.getCurrentAuthToken(), this);
        }
    }
}
